B/blkvt: BlockValues Template. @Purpose: Routines for copying, comparing, creating and destroying block values, and for reading and writing them as if they were arrays. @------------------------------------------------------------------------------- @p Head. As with Flex.i6t, none of this material is worth compiling unless there is a heap on which the block values can be stored. @c #IFDEF MEMORY_HEAP_SIZE; @p Data Section. In this segment, we provide a layer which comes between ``Flex.i6t'' and its eventual users. These are routines which handle the data section for the allocated blocks, and which isolate the end user from having to know how the blocks are divided. As far as the user is concerned, each block value has an ``extent'' $E$, and consists of an array of values indexed from 0 to $E-1$. These are bytes unless |BLK_FLAG_WORD| is set in the header, in which case they are words; unless |BLK_FLAG_16_BIT| is set, in which case they are two-byte quantities. (For instance, indexed text is stored using bytes on the Z-machine but 16-bit quantities on Glulx, since the ZSCII character set can fit into the range 0 to 255 but Unicode cannot.) |BlkValueExtent(B)| returns the value $E$ for the block |B|. |BlkValueSetExtent(B, N)| resizes the block |B| so that value $E$ is at least |N|. If this is a reduction, entries are lost from the end, i.e., from the highest-indexed entries. If it is an expansion, entries are added at the end and the existing entries are preserved. |BlkValueRead(B, I)| returns the value of array entry |I| in |B|. |BlkValueWrite(B, I, V)| puts the value |V| into array entry |I| in |B|. @c [ BlkValueExtent block tsize flags; if (block == 0) return 0; flags = block->BLK_HEADER_FLAGS; if (flags & BLK_FLAG_MULTIPLE == 0) tsize = BlkSize(block) - BLK_DATA_OFFSET; else for (:block~=NULL:block=block-->BLK_NEXT) tsize = tsize + BlkSize(block) - BLK_DATA_MULTI_OFFSET; if (flags & BLK_FLAG_16_BIT) return tsize/2; if (flags & BLK_FLAG_WORD) return tsize/WORDSIZE; return tsize; ]; [ BlkValueSetExtent block tsize flags wsize; if (block == 0) return 0; flags = block->BLK_HEADER_FLAGS; wsize = 1; if (flags & BLK_FLAG_WORD) wsize = WORDSIZE; if (flags & BLK_FLAG_16_BIT) wsize = 2; return BlkResize(block, (tsize)*wsize); ]; [ BlkValueRead block pos dsize hsize flags wsize ot op; if (block==0) rfalse; flags = block->BLK_HEADER_FLAGS; wsize = 1; if (flags & BLK_FLAG_WORD) wsize = WORDSIZE; if (flags & BLK_FLAG_16_BIT) wsize = 2; ot = block; op = pos; pos = pos*wsize; if (flags & BLK_FLAG_MULTIPLE) hsize = BLK_DATA_MULTI_OFFSET; else hsize = BLK_DATA_OFFSET; for (:block~=NULL:block=block-->BLK_NEXT) { dsize = BlkSize(block) - hsize; if ((pos >= 0) && (pos0; 2: #Iftrue (WORDSIZE == 2); return block-->0; #ifnot; return (block->0)*256 + (block->1); #endif; 4: return block-->0; } } pos = pos - dsize; } "*** BlkValueRead: reading from index out of range: ", op, " in ", ot, " ***"; ]; [ BlkValueWrite block pos val dsize hsize flags wsize ot op; if (block==0) rfalse; flags = block->BLK_HEADER_FLAGS; wsize = 1; if (flags & BLK_FLAG_WORD) wsize = WORDSIZE; if (flags & BLK_FLAG_16_BIT) wsize = 2; ot = block; op = pos; pos = pos*wsize; if (flags & BLK_FLAG_MULTIPLE) hsize = BLK_DATA_MULTI_OFFSET; else hsize = BLK_DATA_OFFSET; for (:block~=NULL:block=block-->BLK_NEXT) { dsize = BlkSize(block) - hsize; if ((pos >= 0) && (pos0 = val; 2: #Iftrue (WORDSIZE == 2); block-->0 = val; #ifnot; block->0 = (val/256)%256; block->1 = val%256; #endif; 4: block-->0 = val; } return; } pos = pos - dsize; } "*** BlkValueWrite: writing to index out of range: ", op, " in ", ot, " ***"; ]; @p KOV Support. To carry out the four fundamental operations on block values -- creating, destroying, copying and comparing -- we need to take account of what data they hold. Letting an indexed text go matters little: it contains only character codes. But letting a list of lists of numbers disappear is risky, because we might leave allocated blocks all over the heap for those individual lists of numbers which are now no longer in their parent list. Every KOV has different needs, because every KOV uses its data differently. Each block KOV, then, provides a single ``KOV support'' function. These are named systematically by suffixing |_Support|: that is, the support function for |INDEXED_TEXT_TY| is called |INDEXED_TEXT_TY_Support| and so on. NI compiles a function called |KOVSupportFunction| which takes a KOV number as its argument and returns the relevant function. The support function can be called with any of the following task constants as its first argument: it then has a further one to three arguments depending on the task in hand. @c Constant CREATE_KOVS = 1; Constant CAST_KOVS = 2; Constant DESTROY_KOVS = 3; Constant PRECOPY_KOVS = 4; Constant COPY_KOVS = 5; Constant COMPARE_KOVS = 6; Constant READ_FILE_KOVS = 7; Constant WRITE_FILE_KOVS = 8; Constant HASH_KOVS = 9; @p Creation. To create a new value, we can't simply allocate a block and then fill it in some way appropriate to the kind of value needed: because we don't know what size of block would be a good idea, and even that much depends on the KOV. So the following routine delegates the whole process to the relevant KOV's support routines. Depending on the KOV, we can also create it with a value cast from some existing value: for instance, an indexed text can be created with a value cast from the ordinary text ``Marshmellow''. |K_Support(CREATE_KOVS, cast_from, skov)| -- where |cast_from| and |skov| are optional arguments -- is called to allocate a block on the heap and, if |cast_from| is non-zero, cast that data into the new block value. Of course |cast_from| itself is a value, and we don't specify its KOV: the support function is expected to know what it will get (if anything). |skov| stands for ``strong kind of value'' and is only applicable if |K| is a KOV constructor rather than a base KOV: for instance, if |K| is ``list of...'', then to create a ``list of numbers'' we would have |skov| equal to |NUMBER_TY|, whereas to create a ``list of lists of texts'' |skov| would be |LIST_OF_TY|. @c Global block_value_tally; [ BlkValueCreate kov cast_from skov block sf; if (skov == 0 && (kov < 0 || kov >= BASE_KIND_HWM)) skov = kov; sf = KOVSupportFunction(kov); if (sf) block = sf(CREATE_KOVS, cast_from, skov); else { print "*** Impossible runtime creation ***^"; rfalse; } #ifdef SHOW_ALLOCATIONS; print "[created ", kov, " at ", block, ": ", block_value_tally++, "]^"; #endif; return block; ]; @p Casting. Just as we can create a value with a cast, we can also perform an assignment to an already-created block value in the form of a cast: well, for some kinds of value we can. |K_Support(CAST_KOVS, fromval, fromkov, block)| casts from a value |fromval| with a specified KOV |fromkov| into the existing block value |block|. @c [ BlkValueCast block tokov fromkov fromval sf; sf = KOVSupportFunction(tokov); if (sf) return sf(CAST_KOVS, fromval, fromkov, block); else { print "*** Impossible runtime cast ***^"; rfalse; } ]; @p Destruction. This must be called whenever the current contents of a block value is about to be lost, either because of overwriting or because the block value is in a variable which is going out of scope. What must be done depends on the kind of value: for instance, for indexed text nothing need be done, because the array entries only contains character values. But for a list of lists of numbers, for instance, array entries are pointers to lists of numbers. Simply overwriting or forgetting them means that the lists to which they point will be stuck on the heap forever, and will never be deallocated. So value destruction for a list involves destroying each individual entry and then deallocating it: and this may of course recurse. |K_Support(DESTROY_KOVS, block)| takes whatever action is necessary to ensure that the values remaining in |block| can be discarded without leaving unreferenced data left on the heap. @c [ BlkValueDestroy block k rv sf; if (block == 0) return; k = block-->BLK_HEADER_KOV; sf = KOVSupportFunction(k); if (sf) return sf(DESTROY_KOVS, block); else { print "*** Impossible runtime deallocation ***^"; rfalse; } ]; @p Deep Copy. A deep copy transfers the data contents -- that is, the arrays described above -- from one block to another; the old contents must be destroyed first. Note that we first perform a simplistic copy of the bits over, but that for some block values that won't be good enough. If the two blocks contain lists of lists, then each entry is itself a list, and a deep copy of each entry must also be performed. |K_Support(COPY_KOVS, blockto, blockfrom)| takes whatever additional action might be necessary to ensure that pointers to heap data are not duplicated; it runs after the raw data has been copied bitwise and may only have to correct a few entries, or even none at all. @c [ BlkValueCopy blockto blockfrom dsize i sf; if (blockto == 0) { print "*** Deep copy failed: destination empty ***^"; rfalse; } if (blockfrom == 0) { print "*** Deep copy failed: source empty ***^"; rfalse; } if (blockfrom->BLK_HEADER_N == 0) { ! A hack to handle precompiled array constants: N=0 blocks otherwise don't exist LIST_OF_TY_CopyRawArray(blockto, blockfrom, 1, 0); return blockto; } if (blockfrom-->BLK_HEADER_KOV ~= blockto-->BLK_HEADER_KOV) { print "*** Deep copy failed: types mismatch ***^"; rfalse; } BlkValueDestroy(blockto); dsize = BlkValueExtent(blockfrom); if (((blockfrom->BLK_HEADER_FLAGS) & BLK_FLAG_MULTIPLE) && (BlkValueSetExtent(blockto, dsize, -1) == false)) { print "*** Deep copy failed: resizing failed ***^"; rfalse; } sf = KOVSupportFunction(blockfrom-->BLK_HEADER_KOV); if (sf) sf(PRECOPY_KOVS, blockto, blockfrom); for (i=0:iBLK_HEADER_KOV ~= blockright-->BLK_HEADER_KOV) return blockleft-->BLK_HEADER_KOV - blockright-->BLK_HEADER_KOV; kov = blockleft-->BLK_HEADER_KOV; sf = KOVSupportFunction(kov); if (sf) return sf(COMPARE_KOVS, blockleft, blockright); else { print "*** Impossible runtime comparison ***^"; rfalse; } ]; @p Creating Constants. If the NI compiler reads, say, the string literal ``Sea Devils'' in the source text, and it is expecting to find indexed text -- for instance if it's in a column of a table marked as containing indexed text -- then how is this to find its way into memory as a block value? The heaps starts entirely empty, and NI cannot precompile parts of it: nor would we really want that, since it would hardwire assumptions about the heap's format into NI itself. The answer is that at start-up time we perform creations of all of the constants we will need. Because they are constants, they will never be deallocated and never change in their contents -- so they need not live on the heap at all, and in fact we store them elsewhere in memory. NI simply creates a blank array of the right $2^n$ size. Suppose that |X| is the address of this array. Then during the start-up rules, we at some point do this: |BlkValueInitialCopy(X, BlkValueCreate(INDEXED_TEXT_TY, "Sea Devils"))| The effect is to create a suitable structure on the heap to hold this as indexed text, and then to copy it bitwise into |X|. A bitwise copy is ordinarily against the rules because it duplicates pointers, but of course the created block has been created for this purpose only: nothing points to it, and it is about to vanish -- indeed the address of the block in question exists only briefly on the VM stack. (It is because of this that we don't perform a deep copy using the routine above.) @c [ BlkValueInitialCopy blockto blockfrom dsize i; if (blockto == 0) { print "*** Initial copy failed: destination empty ***^"; rfalse; } if (blockfrom == 0) { print "*** Initial copy failed: source empty ***^"; rfalse; } dsize = 1; for (i=1: i<=blockfrom->BLK_HEADER_N: i++) dsize=dsize*2; for (i=0:ii = blockfrom->i; return blockto; ]; @p Hashing. |K_Support(HASH_KOVS, block)| returns a hash value for the block. The algorithm used for hashing depends on the KOV, but it must have the property that two equivalent blocks (for which |COMPARE_KOVS| returns 0) produce the same hash value. @c [ BlkValueHash block kov sf; if (block == 0) return 0; kov = block-->BLK_HEADER_KOV; sf = KOVSupportFunction(kov); if (sf) return sf(HASH_KOVS, block); else { print "*** Impossible runtime hashing ***^"; rfalse; } ]; [ KOVHashValue kov value; if (KOVIsBlockValue(kov)) return BlkValueHash(value); return value; ]; @p Serialisation. Some block values can be written to external files (on Glulx): others cannot. The following routines abstract that. If |ch| is $-1$, then |K_Support(READ_FILE_KOVS, block, auxf, ch)| returns |true| or |false| according to whether it is possible to read data from an auxiliary file |auxf| into the block value |block|. If |ch| is any other value, then the routine should do exactly that, taking |ch| to be the first character of the text read from the file which makes up the serialised form of the data. |K_Support(WRITE_FILE_KOVS, block)| is simpler because, strictly speaking, it doesn't write to a file at all: it simply prints a serialised form of the data in |block| to the output stream. Since it is called only when that output stream has been redirected to an auxiliary file, and since the serialised form would often be illegible on screen, it seems reasonable to call it a file input-output function just the same. @c [ BlkValueReadFromFile block auxf ch kov sf; sf = KOVSupportFunction(kov); if (sf) return sf(READ_FILE_KOVS, block, auxf, ch); rfalse; ]; [ BlkValueWriteToFile block kov sf; sf = KOVSupportFunction(kov); if (sf) return sf(WRITE_FILE_KOVS, block); rfalse; ]; @p Stubs. To ensure that the template code will still compile if |MEMORY_HEAP_SIZE| is undefined and there's no heap: none of these routines do anything in such a situation, but nor are they ever called -- it's just that I6 source code may refer to them anyway, so they need to exist as routine names. @c #IFNOT; ! IFDEF MEMORY_HEAP_SIZE [ BlkValueReadFromFile; rfalse; ]; [ BlkValueWriteToFile; rfalse; ]; [ BlkValueCreate x y z; ]; [ BlkValueDestroy x; ]; [ BlkValueCopy x y; ]; [ BlkValueCompare x y; ]; #ENDIF; ! IFDEF MEMORY_HEAP_SIZE